
local ltraverse = require("app.public.luatraverse.luatraverse")
local sformat = string.format

local _M = {}

_M.obj_map = nil
_M.ignores = {}
setmetatable(_M.ignores, {__mode = "k"})
_M.ignores[_M.ignores] = true
_M.ignores[_M] = true
_M.ignores[ltraverse] = true
-- _M.ignores[sformat] = true

function _M.add_ignore(obj)
	if obj then
		_M.ignores[obj] = true
	end
end

function _M.snapshot(file)
	local type_cnts = {}
	local function type_inc(t)
		type_cnts[t] = (type_cnts[t] or 0) + 1
	end

	local ex_type_cnts = {}
	local function ex_type_inc(t)
		ex_type_cnts[t] = (ex_type_cnts[t] or 0) + 1
	end

	-- get function path
	local function get_path(obj)
		if type(obj) == "function" then
			local info = debug.getinfo(obj, "nSl")
		    _M.add_ignore(info)
		    if info.what == "Lua" then
		        return info.short_src .. ":" .. info.linedefined
		    end
		end
	    return nil
	end

	-- build metatable->type map for userdata type detection.
	local ud_types = {}
	local reg = debug.getregistry()
	for k,v in pairs(reg) do
		if type(k) == 'string' and type(v) == 'table' then
			ud_types[v] = k
		end
	end
	local function ud_type(ud)
		return ud_types[debug.getmetatable(ud)] or "[unknown]"
	end

	-- dump(ud_types, "ud_types")

	local function get_name(ud)
		local name = ud_types[ud]
		-- name = name or ud_types[debug.getmetatable(ud)]
		name = name or "<unknown>"
		return name
	end

	local obj_map = {}
    setmetatable(obj_map, {__mode = "k"})
    local function get_data(obj)
	    if nil == obj then return nil end
	    local data = obj_map[obj]
	    if not data then
	        local refs = {}
	        setmetatable(refs, {__mode = "k"})
	        data = {name="", child=0, refs=refs, refnum=0}
	        data.path = get_path(obj)
	        data.type = ud_type(obj)
	        obj_map[obj] = data
	        
	        _M.add_ignore(data)
	        _M.add_ignore(refs)
	    end
	    return data
	end

	local function is_care_type(type)
		return type == "table" or type == "function" 
			or type == "userdata" or type == "thread"
	end

	local function compare_name(old, new)
		if #old == 0 then
			return new
		else
			local sIdx1, eIdx1, catch1 = string.find(old, "(%[.-%])")
			local sIdx2, eIdx2, catch2 = string.find(new, "(%[.-%])")
			if sIdx1 ~= nil and sIdx2 == nil then
				return new
			end
			return old
		end
	end

	local str_data = 0

	local funcs = {
		-- how = key value isname local ismetatable environment upvalue
		["edge"] = function(from, to, how, name)
			type_inc"edges"

			if _M.ignores[to] then return end

			local f_data = get_data(from)

			if is_care_type(type(to)) then
				local t_data = get_data(to)
				if how == "key" then
					if f_data then
						local ref = f_data.name .. "/[key]"
						t_data.name = compare_name(t_data.name, ud_types[to] or ref)
						if not t_data.refs[from] then
							t_data.refnum = t_data.refnum + 1
						end
						t_data.refs[from] = ref
					else

					end
				elseif how == "value" then
					if f_data then
						f_data.child = f_data.child + 1
						local ref = f_data.name .. "/" .. tostring(name)
						t_data.name = compare_name(t_data.name, ud_types[to] or ref)
						if not t_data.refs[from] then
							t_data.refnum = t_data.refnum + 1
						end
						t_data.refs[from] = ref
					else
						t_data.name = tostring(name)
					end
				elseif how == "isname" then
					if f_data then
						local ref = f_data.name .. "/[name]"
						t_data.name = compare_name(t_data.name, ud_types[to] or ref)
						if not t_data.refs[from] then
							t_data.refnum = t_data.refnum + 1
						end
						t_data.refs[from] = ref
					else
						t_data.name = compare_name(t_data.name, ud_types[to] or "[name]")
					end
				elseif how == "local" then
					if f_data then

					else
						t_data.name = compare_name(t_data.name, ud_types[to] or "[local]/" .. tostring(name))
					end
				elseif how == "ismetatable" then
					if f_data then
						local ref = f_data.name .. "/[meta]"
						t_data.name = compare_name(t_data.name, ud_types[to] or ref)
						if not t_data.refs[from] then
							t_data.refnum = t_data.refnum + 1
						end
						t_data.refs[from] = ref
					else

					end
				elseif how == "environment" then
					if f_data then
						local ref = f_data.name .. "/[env]"
						t_data.name = compare_name(t_data.name, ud_types[to] or ref)
						if not t_data.refs[from] then
							t_data.refnum = t_data.refnum + 1
						end
						t_data.refs[from] = ref
					else

					end
				elseif how == "upvalue" then
					if f_data then
						local ref = f_data.name .. "/[up]" .. tostring(name)
						t_data.name = compare_name(t_data.name, ud_types[to] or ref)
						if not t_data.refs[from] then
							t_data.refnum = t_data.refnum + 1
						end
						t_data.refs[from] = ref
					else

					end
				end
			end
		end,
		["table"] = function(v)
			type_inc"table"
		end,
		["string"] = function(v)
			type_inc"string"
			str_data = str_data + #v
		end,
		["userdata"] = function(v)
			type_inc"userdata"
			-- type_inc(ud_type(v))
			ex_type_inc(ud_type(v))
		end,
		["cdata"] = function(v)
			type_inc"cdata"
		end,
		["func"] = function(v)
			type_inc"function"
		end,
		["thread"] = function(v)
			type_inc"thread"
		end,
	}

	for k,v in pairs(funcs) do
		_M.add_ignore(k)
		_M.add_ignore(v)
	end
	_M.add_ignore(type_cnts)
	_M.add_ignore(funcs)
	_M.add_ignore(ud_types)
	_M.add_ignore(ud_type)

	_M.add_ignore(type_inc)
	_M.add_ignore(ex_type_inc)
	_M.add_ignore(obj_map)
	_M.add_ignore(ex_type_cnts)
	_M.add_ignore(get_name)
	_M.add_ignore(is_care_type)
	_M.add_ignore(get_data)
	_M.add_ignore(get_path)
	_M.add_ignore(compare_name)

	_M.add_ignore(reg)

	ltraverse.traverse(funcs, _M.ignores)

	-- local fd = file
	-- if type(file) == 'string' then
	-- 	fd = io.open(file, "w")
	-- end
	-- fd:write(sformat("memory = %i bytes\n", collectgarbage"count" * 1024))
	-- fd:write(sformat("str_data = %i\n", str_data))
	-- fd:write(sformat("object type counts:\n"))
	-- for t,cnt in pairs(type_cnts) do
	-- 	fd:write(sformat("  %s = %9i\n", t, cnt))
	-- end
	-- for t,cnt in pairs(ex_type_cnts) do
	-- 	fd:write(sformat("  %s = %9i\n", t, cnt))
	-- end
	-- fd:write("\n")

	-- for k,data in pairs(obj_map) do
	-- 	fd:write(sformat("%5s : %3i %3i\n", data.name, data.child, #data.names))
	-- 	if #data.names > 0 then
	-- 		for i,name in ipairs(data.names) do
	-- 			fd:write(sformat("%5i : %s\n", i, name))
	-- 		end
	-- 	end
	-- end

	-- fd:write("\n")

	-- --[[
	-- fd:write("LUA_REGISTRY dump:\n")
	-- for k,v in pairs(reg) do
	-- 	fd:write(tostring(k),'=', tostring(v),'\n')
	-- end
	-- --]]
	-- if type(file) == 'string' then
	-- 	fd:close()
	-- end
	return obj_map
end

return _M
